package com.xyy.saas.payment.payment.core.service;

import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import com.xyy.saas.payment.common.ReconcileTradeConstants;
import com.xyy.saas.payment.common.enums.ReconcileTradeErrorDataTypeEnum;
import com.xyy.saas.payment.common.enums.ReconcileTradeErrorProcessStatus;
import com.xyy.saas.payment.common.enums.ReconcileTradeErrorTypeEnum;
import com.xyy.saas.payment.common.enums.ReconciliationType;
import com.xyy.saas.payment.common.exception.PaymentException;
import com.xyy.saas.payment.cores.enums.BusinessIdTypeEnum;
import com.xyy.saas.payment.cores.enums.FbankTradeCodeEnum;
import com.xyy.saas.payment.cores.enums.ResultCodeEnum;
import com.xyy.saas.payment.dao.mapper.*;
import com.xyy.saas.payment.dao.model.*;
import com.xyy.saas.payment.dto.ReconcileTradeDto;
import com.xyy.saas.payment.payment.core.dto.ReconcileContext;
import com.xyy.saas.payment.trace.AlarmService;
import com.xyy.saas.payment.trace.DingTalkMessage;
import com.xyy.saas.payment.util.AmountCastUtil;
import com.xyy.saas.payment.util.ListUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;

/**
 * @author wangzeqi
 */
@Slf4j
@Service
public class TradeCheckService2 {

    @Resource
    private FbankPayRecordsMapper fbankPayRecordsMapper;
    @Resource
    private SeparateOrderMapper separateOrderMapper;
    @Resource
    private PayResultOrderMapper payResultOrderMapper;
    @Resource
    private RefundOrderMapper refundOrderMapper;
    @Resource
    private TradeErrorRecordsMapper tradeErrorRecordsMapper;
    @Resource
    private AlarmService alarmService;
    @Value("${reconcile.webChatWebHook:''}")
    private String webChatWebHook;
    @Value("${reconcile.webChatMobiles:''}")
    private String webChatMobiles;
    @Value("#{'${reconciliation.tradeCheckExcludeAccountIds:138c}'.split(',')}")
    private List<String> tradeCheckExcludeAccountIds;


    /**
     * 按交易类型核对交易明细
     * fbank_pay_records separate_order pay_result_order refund_order
     */
    public void reconcileTradeByType(ReconcileContext context) {
        checkReconcileTradeParamDto(context);

        // fbank_pay_records -- separate_order
        try {
            long startTime = System.currentTimeMillis();
            reconcileTradeDetailsWithFbank2SeparateOrder(context);
            log.info("reconcileTradeDetails - Method reconcileTradeDetailsWithFbank2SeparateOrder cost time: {}", System.currentTimeMillis() - startTime);
        } catch (Exception e) {
            log.error("reconcileTradeDetails - Method reconcileTradeDetailsWithFbank2SeparateOrder error, context:{}", JSON.toJSONString(context), e);
        }

        // fbank_pay_records -- pay_result_order

        try {
            long startTime = System.currentTimeMillis();
            reconcileTradeDetailsWithFbank2PayResultOrder(context);
            log.info("reconcileTradeDetails - Method reconcileTradeDetailsWithFbank2PayResultOrder cost time: {}", System.currentTimeMillis() - startTime);
        } catch (Exception e) {
            log.error("reconcileTradeDetails - Method reconcileTradeDetailsWithFbank2PayResultOrder error, context:{}", JSON.toJSONString(context), e);
        }

        // fbank_pay_records -- refund_order
        try {
            long startTime = System.currentTimeMillis();
            reconcileTradeDetailsWithFbank2RefundOrder(context);
            log.info("reconcileTradeDetails - Method reconcileTradeDetailsWithFbank2RefundOrder cost time: {}", System.currentTimeMillis() - startTime);
        } catch (Exception e) {
            log.error("reconcileTradeDetails - Method reconcileTradeDetailsWithFbank2RefundOrder error, context:{}", JSON.toJSONString(context), e);
        }
    }

    @SneakyThrows
    private void checkReconcileTradeParamDto(ReconcileContext context) {
        if (context == null) {
            throw new PaymentException(ResultCodeEnum.PARAM_ERROR);
        }

        LocalDateTime beginTime = context.getBeginTime();
        LocalDateTime endTime = context.getEndTime();
        if (beginTime == null || endTime == null) {
            endTime = LocalDate.now().atStartOfDay();
            beginTime = endTime.minusDays(1);
        }

        if (Math.abs(ChronoUnit.DAYS.between(beginTime, endTime)) > 30) {
            log.error("reconcileTradeDetails - Method checkReconcileTradeParamDto time diff days > 30");
            throw new PaymentException(ResultCodeEnum.PARAM_ERROR);
        }

    }

    // fbank_pay_records -- refund_order
    public void reconcileTradeDetailsWithFbank2RefundOrder(ReconcileContext context) {
        log.info("reconcileTradeDetails - Method reconcileTradeDetailsWithFbank2RefundOrder start, context:{}", JSON.toJSONString(context));

        List<TradeErrorRecords> resultList = new ArrayList<>();

        // fbank单边
        try {
            List<TradeErrorRecords> fankSingleSideRecords = findFankSingleSideRefundOrder(context.getBeginTime(), context.getEndTime());
            resultList.addAll(fankSingleSideRecords);
        } catch (Exception e) {
            log.error("reconcileTradeDetails - Method findFankSingleSideRefundOrder failed with exception: {}", e.getMessage(), e);
        }

        // refund_order单边
        try {
            List<TradeErrorRecords> payResultSingleSideRecords = findRefundOrderSingleSide(context.getBeginTime(), context.getEndTime());
            resultList.addAll(payResultSingleSideRecords);
        } catch (Exception e) {
            log.error("reconcileTradeDetails - Method findRefundOrderSingleSide failed with exception: {}", e.getMessage(), e);
        }

        // 错账
        try {
            List<TradeErrorRecords> errorRecords = findFbank2RefundOrderErrorRecords(context.getBeginTime(), context.getEndTime());
            resultList.addAll(errorRecords);
        } catch (Exception e) {
            log.error("reconcileTradeDetails - Method findFbank2RefundOrderErrorRecords failed with exception: {}", e.getMessage(), e);
        }

        if (CollectionUtils.isNotEmpty(resultList)) {
            log.warn("reconcileTradeDetails - Method reconcileTradeDetailsWithFbank2RefundOrder errorRecords size:{}, context:{}", resultList.size(), JSON.toJSONString(context));

            alarmService.alarm(webChatWebHook, webChatMobiles,
                    DingTalkMessage.builder()
                            .type("交易明细对账")
                            .method("reconcileTradeDetailsWithFbank2RefundOrder")
                            .msg("fbank_pay_records 和 refund_order 对账存在差异数据，共计" + resultList.size() + "条")
                            .build());

            int maxLimit = 200;
            for (List<TradeErrorRecords> tradeErrorRecords : Lists.partition(resultList, maxLimit)) {
                tradeErrorRecordsMapper.batchInsert(tradeErrorRecords);
            }
        }
    }

    private List<TradeErrorRecords> findFbank2RefundOrderErrorRecords(LocalDateTime beginTime, LocalDateTime endTime) {
        List<ReconcileTradeDto> errorAmountDtos = fbankPayRecordsMapper.queryFbank2RefundOrderErrorAmountDtos(beginTime, endTime,
                ReconcileTradeConstants.reconcileRefundOrderOrderTradeCodes,tradeCheckExcludeAccountIds);

        if (CollectionUtils.isEmpty(errorAmountDtos)) {
            return Collections.emptyList();
        }

        // 填充
        List<TradeErrorRecords> tradeErrorRecords = new ArrayList<>();
        fillFbank2TradeErrorAmountDtoList(tradeErrorRecords, errorAmountDtos);
        return tradeErrorRecords;
    }

    private List<TradeErrorRecords> findRefundOrderSingleSide(LocalDateTime beginTime, LocalDateTime endTime) {
        // 查 refundOrder 单边记录

        // 冻结充值撤销、在途充值撤销
        List<RefundOrder> refundOrders = refundOrderMapper.queryRefundOrderSingleSide1(beginTime, endTime,
                ReconcileTradeConstants.reconcilePayResultOrderPayChannels1,
                Lists.newArrayList(FbankTradeCodeEnum.FTC_5.getCode(), FbankTradeCodeEnum.FTC_3.getCode()),tradeCheckExcludeAccountIds);

        // 见证+收单退款
        List<RefundOrder> refundOrders2 = refundOrderMapper.queryRefundOrderSingleSide2(beginTime, endTime,
                ReconcileTradeConstants.reconcilePayResultOrderPayChannels2,
                Lists.newArrayList(FbankTradeCodeEnum.FTC_6.getCode()),tradeCheckExcludeAccountIds);

        // merge
        List<RefundOrder> allSingleSideRefundOrders = ListUtils.mergeLists(refundOrders, refundOrders2);
        if (CollectionUtils.isEmpty(allSingleSideRefundOrders)) {
            return Collections.emptyList();
        }

        // 填充结果
        List<TradeErrorRecords> tradeErrorRecords = new ArrayList<>();
        fillRefundOrderSingleSideErrorList(tradeErrorRecords, allSingleSideRefundOrders);
        return tradeErrorRecords;
    }

    private void fillRefundOrderSingleSideErrorList(List<TradeErrorRecords> tradeErrorRecords, List<RefundOrder> allSingleSideRefundOrders) {
        for (RefundOrder refundOrder : allSingleSideRefundOrders) {
            Integer tradeCode = getTradeCodeByRefundOrder(refundOrder);
            TradeErrorRecords dto = new TradeErrorRecords()
                    .setBizNo(refundOrder.getRefundNo())
                    //.setYbmOrderNo()
                    .setDataType(ReconcileTradeErrorDataTypeEnum.TRADE_ERROR.getCode())
                    .setErrorType(ReconcileTradeErrorTypeEnum.SINGLE_SIDE.getCode())
                    .setErrorDescription(String.join("|",
                            ReconcileTradeErrorDataTypeEnum.TRADE_ERROR.getType(),
                            FbankTradeCodeEnum.code2DescMap.get(tradeCode),
                            ReconcileTradeErrorTypeEnum.SINGLE_SIDE.getType(),
                            refundOrder.getRefundNo()))
                    .setItemCode(ReconciliationType.TRADE_DETAIL.name())
                    .setTradeCode(tradeCode)
                    //.setBizCode()
                    .setProcessStatus(ReconcileTradeErrorProcessStatus.PENDING.getCode())
                    .setExpectedAmount(BigDecimal.ZERO)
                    .setActualAmount(AmountCastUtil.castFen2YuanByBigDecimal(refundOrder.getAmount()))
                    .setTradeTime(refundOrder.getSuccessTime())
                    .setReconcileDate(new Date());
            tradeErrorRecords.add(dto);
        }
    }

    private Integer getTradeCodeByRefundOrder(RefundOrder refundOrder) {
        String businessOrderType = refundOrder.getBusinessOrderType();
        String channel = refundOrder.getChannel();
        if (StringUtils.isBlank(businessOrderType) || StringUtils.isBlank(channel)) {
            log.error("reconcileTradeDetails - Method getTradeCodeByRefundOrder businessOrderType or payChannel is blank, refundNo:{}", refundOrder.getRefundNo());
            return FbankTradeCodeEnum.FTC_99.getCode();
        }

        // 需充值的三方支付渠道
        boolean isNeedRechargePayChannel = ReconcileTradeConstants.reconcilePayResultOrderPayChannels1.contains(channel);
        // 平安支付渠道
        boolean isPinganPayChannel = ReconcileTradeConstants.reconcilePayResultOrderPayChannels2.contains(channel);
        boolean isPop = Objects.equals(businessOrderType, BusinessIdTypeEnum.EC_POP.getType());
        boolean isSelfSupport = businessOrderType.contains(BusinessIdTypeEnum.EC_SELF_SUPPORT.getType());

        // 冻结充值撤销
        if (isNeedRechargePayChannel && isPop) {
            return FbankTradeCodeEnum.FTC_5.getCode();
        }
        // 在途充值撤销
        if (isNeedRechargePayChannel && isSelfSupport) {
            return FbankTradeCodeEnum.FTC_3.getCode();
        }
        // 见证+收单退款
        if (isPinganPayChannel) {
            return FbankTradeCodeEnum.FTC_6.getCode();
        }

        log.error("reconcileTradeDetails - Method getTradeCodeByRefundOrder tradeCode unknown, refundNo:{}", refundOrder.getRefundNo());
        return FbankTradeCodeEnum.FTC_99.getCode();
    }

    private List<TradeErrorRecords> findFankSingleSideRefundOrder(LocalDateTime beginTime, LocalDateTime endTime) {
        // 冻结充值撤销、在途充值撤销
        // 见证+收单退款
        List<FbankPayRecords> unmatchedFbankPayRecords2 = fbankPayRecordsMapper.queryFbankSingleSideWithRefundOrder2(beginTime, endTime,
                ReconcileTradeConstants.reconcileRefundOrderOrderTradeCodes,tradeCheckExcludeAccountIds);

        if (CollectionUtils.isEmpty(unmatchedFbankPayRecords2)) {
            return Collections.emptyList();
        }
        List<TradeErrorRecords> tradeErrorRecords = new ArrayList<>();
        fillFankSingleSideReconcileTradeErrorDtoList(tradeErrorRecords, unmatchedFbankPayRecords2);
        return tradeErrorRecords;
    }

    // fbank_pay_records -- pay_result_order
    public void reconcileTradeDetailsWithFbank2PayResultOrder(ReconcileContext context) {
        log.info("reconcileTradeDetails - Method reconcileTradeDetailsWithFbank2PayResultOrder start, context:{}", JSON.toJSONString(context));

        List<TradeErrorRecords> resultList = new ArrayList<>();

        // fbank单边
        try {
            List<TradeErrorRecords> fankSingleSideRecords = findFankSingleSidePayResult(context.getBeginTime(), context.getEndTime());
            resultList.addAll(fankSingleSideRecords);
        } catch (Exception e) {
            log.error("reconcileTradeDetails - Method findFankSingleSidePayResult failed with exception: {}", e.getMessage(), e);
        }

        // pay_result_order单边
        try {
            List<TradeErrorRecords> payResultSingleSideRecords = findPayResultSingleSide(context.getBeginTime(), context.getEndTime());
            resultList.addAll(payResultSingleSideRecords);
        } catch (Exception e) {
            log.error("reconcileTradeDetails - Method findPayResultSingleSide failed with exception: {}", e.getMessage(), e);
        }

        // 错账
        try {
            List<TradeErrorRecords> errorRecords = findFbank2PayResultErrorRecords(context.getBeginTime(), context.getEndTime());
            resultList.addAll(errorRecords);
        } catch (Exception e) {
            log.error("reconcileTradeDetails - Method findFbank2PayResultErrorRecords failed with exception: {}", e.getMessage(), e);
        }

        if (CollectionUtils.isNotEmpty(resultList)) {
            log.warn("reconcileTradeDetails - Method reconcileTradeDetailsWithFbank2PayResultOrder errorRecords size:{}, context:{}", resultList.size(), JSON.toJSONString(context));

            alarmService.alarm(webChatWebHook, webChatMobiles,
                    DingTalkMessage.builder()
                            .type("交易明细对账")
                            .method("reconcileTradeDetailsWithFbank2PayResultOrder")
                            .msg("fbank_pay_records 和 pay_result_order 对账存在差异数据，共计" + resultList.size() + "条")
                            .build());

            int maxLimit = 200;
            for (List<TradeErrorRecords> tradeErrorRecords : Lists.partition(resultList, maxLimit)) {
                tradeErrorRecordsMapper.batchInsert(tradeErrorRecords);
            }
        }
    }

    private List<TradeErrorRecords> findFbank2PayResultErrorRecords(LocalDateTime beginTime, LocalDateTime endTime) {
        // 查询错账记录
        List<ReconcileTradeDto> errorAmountDtos = fbankPayRecordsMapper.queryFbank2PayResultErrorAmountRecords(beginTime, endTime, ReconcileTradeConstants.reconcilePayResultOrderOrderTradeCodes,
                tradeCheckExcludeAccountIds);

        if (CollectionUtils.isEmpty(errorAmountDtos)) {
            return Collections.emptyList();
        }

        // 填充
        List<TradeErrorRecords> tradeErrorRecords = new ArrayList<>();
        fillFbank2TradeErrorAmountDtoList(tradeErrorRecords, errorAmountDtos);
        return tradeErrorRecords;
    }

    private List<TradeErrorRecords> findPayResultSingleSide(LocalDateTime beginTime, LocalDateTime endTime) {
        // 查询 payResultOrder 单边记录

        // 1.冻结充值、在途充值
        List<PayResultOrder> payResultOrders = payResultOrderMapper.queryPayResultOrderSingleSide1(beginTime, endTime,
                ReconcileTradeConstants.reconcilePayResultOrderPayChannels1,
                Lists.newArrayList(FbankTradeCodeEnum.FTC_10.getCode(), FbankTradeCodeEnum.FTC_4.getCode()),tradeCheckExcludeAccountIds);

        // 2.见证+收单充值、见证+收单资金冻结、见证+收单资金流水
        List<PayResultOrder> payResultOrders2 = payResultOrderMapper.queryPayResultOrderSingleSide2(beginTime, endTime,
                ReconcileTradeConstants.reconcilePayResultOrderPayChannels2,
                Lists.newArrayList(FbankTradeCodeEnum.FTC_9.getCode(), FbankTradeCodeEnum.FTC_8.getCode(), FbankTradeCodeEnum.FTC_7.getCode()),tradeCheckExcludeAccountIds);

        // 合并
        List<PayResultOrder> unmatchedAllRecords = ListUtils.mergeLists(payResultOrders, payResultOrders2);
        if (CollectionUtils.isEmpty(unmatchedAllRecords)) {
            return Collections.emptyList();
        }

        // 填充结果
        List<TradeErrorRecords> tradeErrorRecords = new ArrayList<>();
        fillPayResultOrderSingleSideErrorList(beginTime, tradeErrorRecords, unmatchedAllRecords);

        return tradeErrorRecords;
    }

    private void fillPayResultOrderSingleSideErrorList(LocalDateTime beginTime, List<TradeErrorRecords> tradeErrorRecords, List<PayResultOrder> unmatchedAllRecords) {
        for (PayResultOrder payResultOrder : unmatchedAllRecords) {
            Integer tradeCode = getTradeCodeByPayResultOrder(payResultOrder);
            TradeErrorRecords dto = new TradeErrorRecords()
                    .setBizNo(payResultOrder.getPayNo())
                    //.setYbmOrderNo()
                    .setDataType(ReconcileTradeErrorDataTypeEnum.TRADE_ERROR.getCode())
                    .setErrorType(ReconcileTradeErrorTypeEnum.SINGLE_SIDE.getCode())
                    .setErrorDescription(String.join("|",
                            ReconcileTradeErrorDataTypeEnum.TRADE_ERROR.getType(),
                            FbankTradeCodeEnum.code2DescMap.get(tradeCode),
                            ReconcileTradeErrorTypeEnum.SINGLE_SIDE.getType(),
                            payResultOrder.getPayNo()))
                    .setItemCode(ReconciliationType.TRADE_DETAIL.name())
                    .setTradeCode(tradeCode)
                    //.setBizCode()
                    .setProcessStatus(ReconcileTradeErrorProcessStatus.PENDING.getCode())
                    .setExpectedAmount(BigDecimal.ZERO)
                    .setActualAmount(AmountCastUtil.castFen2YuanByBigDecimal(payResultOrder.getAmount()))
                    .setTradeTime(payResultOrder.getCtime())
                    .setReconcileDate(new Date());
            tradeErrorRecords.add(dto);
        }
    }

    private Integer getTradeCodeByPayResultOrder(PayResultOrder payResultOrder) {
        String businessOrderType = payResultOrder.getBusinessOrderType();
        String payChannel = payResultOrder.getPayChannel();
        if (StringUtils.isBlank(businessOrderType) || StringUtils.isBlank(payChannel)) {
            log.error("reconcileTradeDetails - Method getTradeCodeByPayResultOrder businessOrderType or payChannel is blank, payNo:{}", payResultOrder.getPayNo());
            return FbankTradeCodeEnum.FTC_99.getCode();
        }

        // 需充值的三方支付渠道
        boolean isNeedRechargePayChannel = ReconcileTradeConstants.reconcilePayResultOrderPayChannels1.contains(payChannel);
        // 平安支付渠道
        boolean isPinganPayChannel = ReconcileTradeConstants.reconcilePinganPayChannels.contains(payChannel);
        boolean isPop = Objects.equals(businessOrderType, BusinessIdTypeEnum.EC_POP.getType());
        boolean isSelfSupport = businessOrderType.contains(BusinessIdTypeEnum.EC_SELF_SUPPORT.getType());

        // 冻结充值
        if (isNeedRechargePayChannel && isPop) {
            return FbankTradeCodeEnum.FTC_10.getCode();
        }
        // 在途充值
        if (isNeedRechargePayChannel && isSelfSupport) {
            return FbankTradeCodeEnum.FTC_4.getCode();
        }
        // 见证+收单充值
        if (isPinganPayChannel && isSelfSupport) {
            return FbankTradeCodeEnum.FTC_9.getCode();
        }
        // 见证+收单资金冻结
        if (isPinganPayChannel && isPop) {
            return FbankTradeCodeEnum.FTC_8.getCode();
        }
        // 见证+收单资金流水
        if (Objects.equals(payChannel, ReconcileTradeConstants.pinganCreditPayChannel)) {
            return FbankTradeCodeEnum.FTC_7.getCode();
        }

        log.error("reconcileTradeDetails - Method getTradeCodeByPayResultOrder tradeCode unknown, payNo:{}", payResultOrder.getPayNo());
        return FbankTradeCodeEnum.FTC_99.getCode();
    }

    private List<TradeErrorRecords> findFankSingleSidePayResult(LocalDateTime beginTime, LocalDateTime endTime) {
        // 查询fbank单边
        List<FbankPayRecords> unmatchedFbankPayRecords = fbankPayRecordsMapper.queryFbankSingleSideWithPayResult(beginTime, endTime,
                ReconcileTradeConstants.reconcilePayResultOrderOrderTradeCodes,tradeCheckExcludeAccountIds);
        if (CollectionUtils.isEmpty(unmatchedFbankPayRecords)) {
            return Collections.emptyList();
        }

        // 填充结果
        List<TradeErrorRecords> tradeErrorRecords = new ArrayList<>();
        fillFankSingleSideReconcileTradeErrorDtoList(tradeErrorRecords, unmatchedFbankPayRecords);
        return tradeErrorRecords;
    }

    // fbank_pay_records -- separate_order
    public void reconcileTradeDetailsWithFbank2SeparateOrder(ReconcileContext context) {
        log.info("reconcileTradeDetails - Method reconcileTradeDetailsWithFbank2SeparateOrder start, context:{}", JSON.toJSONString(context));

        List<TradeErrorRecords> resultList = new ArrayList<>();

        // 查fbank单边记录
        try {
            List<TradeErrorRecords> fankSingleSideRecords = findFankSingleSideSeparateOrderRecords(context.getBeginTime(), context.getEndTime());
            resultList.addAll(fankSingleSideRecords);
        } catch (Exception e) {
            log.error("reconcileTradeDetails - Method findFankSingleSideSeparateOrderRecords failed with exception: {}", e.getMessage(), e);
        }

        // 查separateOrder单边记录
        try {
            List<TradeErrorRecords> separateOrderSingleSideRecords = findSeparateOrderSingleSideRecords(context.getBeginTime(), context.getEndTime());
            resultList.addAll(separateOrderSingleSideRecords);
        } catch (Exception e) {
            log.error("reconcileTradeDetails - Method findSeparateOrderSingleSideRecords failed with exception: {}", e.getMessage(), e);
        }

        // 查询错账记录
        try {
            List<TradeErrorRecords> errorRecords = findFbank2SeparateOrderErrorRecords(context.getBeginTime(), context.getEndTime());
            resultList.addAll(errorRecords);
        } catch (Exception e) {
            log.error("reconcileTradeDetails - Method findFbank2SeparateOrderErrorRecords failed with exception: {}", e.getMessage(), e);
        }

        if (CollectionUtils.isNotEmpty(resultList)) {
            log.warn("reconcileTradeDetails - Method reconcileTradeDetailsWithFbank2SeparateOrder errorRecords size:{}, context:{}", resultList.size(), JSON.toJSONString(context));

            alarmService.alarm(webChatWebHook, webChatMobiles,
                    DingTalkMessage.builder()
                            .type("交易明细对账")
                            .method("reconcileTradeDetailsWithFbank2SeparateOrder")
                            .msg("fbank_pay_records 和 separate_order 对账存在差异数据，共计" + resultList.size() + "条")
                            .build());

            int maxLimit = 200;
            for (List<TradeErrorRecords> tradeErrorRecords : Lists.partition(resultList, maxLimit)) {
                tradeErrorRecordsMapper.batchInsert(tradeErrorRecords);
            }
        }
    }

    private List<TradeErrorRecords> findFbank2SeparateOrderErrorRecords(LocalDateTime beginTime, LocalDateTime endTime) {
        // 查询错账记录
        List<ReconcileTradeDto> errorAmountDtos = fbankPayRecordsMapper.queryFbank2SeparateOrderErrorAmountRecords(beginTime, endTime,
                ReconcileTradeConstants.reconcileSeparateOrderTradeCodes1);

        // 会员间交易退款错账记录
        List<ReconcileTradeDto> errorAmountDtos2 = fbankPayRecordsMapper.queryFbank2SeparateOrderErrorAmountWithRefundRecords(beginTime, endTime);

        // 合并
        List<ReconcileTradeDto> allErrorAmountDtos = ListUtils.mergeLists(errorAmountDtos, errorAmountDtos2);

        if (CollectionUtils.isEmpty(allErrorAmountDtos)) {
            return Collections.emptyList();
        }

        // 填充
        List<TradeErrorRecords> reconcileTradeErrorDtoList = new ArrayList<>();
        fillFbank2TradeErrorAmountDtoList(reconcileTradeErrorDtoList, allErrorAmountDtos);

        return reconcileTradeErrorDtoList;
    }

    private void fillFbank2TradeErrorAmountDtoList(List<TradeErrorRecords> reconcileTradeErrorDtoList,
                                                   List<ReconcileTradeDto> allErrorAmountDtos) {
        for (ReconcileTradeDto fromDto : allErrorAmountDtos) {
            TradeErrorRecords dto = new TradeErrorRecords()
                    .setBizNo(fromDto.getBizNo())
                    //.setYbmOrderNo()
                    .setDataType(ReconcileTradeErrorDataTypeEnum.TRADE_ERROR.getCode())
                    .setErrorType(ReconcileTradeErrorTypeEnum.ERROR_ACCOUNT.getCode())
                    .setErrorDescription(String.join("|",
                            ReconcileTradeErrorDataTypeEnum.TRADE_ERROR.getType(),
                            FbankTradeCodeEnum.code2DescMap.get(fromDto.getTradeCode()),
                            ReconcileTradeErrorTypeEnum.ERROR_ACCOUNT.getType(),
                            fromDto.getBizNo()))
                    .setItemCode(ReconciliationType.TRADE_DETAIL.name())
                    .setTradeCode(fromDto.getTradeCode())
                    //.setBizCode()
                    .setProcessStatus(ReconcileTradeErrorProcessStatus.PENDING.getCode())
                    .setExpectedAmount(AmountCastUtil.castFen2YuanByBigDecimal(fromDto.getExpectedAmount()))  // 预期金额--交易金额
                    .setActualAmount(AmountCastUtil.castFen2YuanByBigDecimal(fromDto.getActualAmount()))  // 实际金额--清算金额
                    .setTradeTime(fromDto.getTradeTime())
                    .setReconcileDate(new Date());
            reconcileTradeErrorDtoList.add(dto);
        }
    }

    private List<TradeErrorRecords> findSeparateOrderSingleSideRecords(LocalDateTime beginTime, LocalDateTime endTime) {
        // 查单边记录
        List<SeparateOrder> unmatchedSeparateOrders = separateOrderMapper.querySeparateOrderSingleSide(beginTime, endTime,
                ReconcileTradeConstants.reconcileSeparateOrderTradeCodes1);

        // 平安贷交易退款单边记录 (特殊处理)
        List<SeparateOrder> unmatchedRefundSeparateOrders = separateOrderMapper.querySeparateOrderSingleSideWithRefund(beginTime, endTime);
        // 会员间交易退款单边记录 (特殊处理)
        List<SeparateOrder> unmatchedRefundSeparateOrders2 = separateOrderMapper.querySeparateOrderSingleSideWithRefund2(beginTime, endTime);

        // 合并
        List<SeparateOrder> unmatchedAllRecords = ListUtils.mergeLists(unmatchedSeparateOrders, unmatchedRefundSeparateOrders, unmatchedRefundSeparateOrders2);
        if (CollectionUtils.isEmpty(unmatchedAllRecords)) {
            return Collections.emptyList();
        }

        // 填充
        List<TradeErrorRecords> reconcileTradeErrorDtoList = new ArrayList<>();
        fillSeparateOrderSingleSideErrorList(reconcileTradeErrorDtoList, unmatchedAllRecords);
        return reconcileTradeErrorDtoList;
    }

    private void fillSeparateOrderSingleSideErrorList(List<TradeErrorRecords> reconcileTradeErrorDtoList, List<SeparateOrder> unmatchedSeparateOrders) {
        for (SeparateOrder separateOrder : unmatchedSeparateOrders) {
            TradeErrorRecords dto = new TradeErrorRecords()
                    .setBizNo(separateOrder.getBizNo())
                    //.setYbmOrderNo()
                    .setDataType(ReconcileTradeErrorDataTypeEnum.TRADE_ERROR.getCode())
                    .setErrorType(ReconcileTradeErrorTypeEnum.SINGLE_SIDE.getCode())
                    .setErrorDescription(String.join("|",
                            ReconcileTradeErrorDataTypeEnum.TRADE_ERROR.getType(),
                            FbankTradeCodeEnum.code2DescMap.get(separateOrder.getTradeCode()),
                            ReconcileTradeErrorTypeEnum.SINGLE_SIDE.getType(),
                            separateOrder.getBizNo()))
                    .setItemCode(ReconciliationType.TRADE_DETAIL.name())
                    .setTradeCode(separateOrder.getTradeCode())
                    //.setBizCode()
                    .setProcessStatus(ReconcileTradeErrorProcessStatus.PENDING.getCode())
                    .setExpectedAmount(BigDecimal.ZERO)
                    .setActualAmount(AmountCastUtil.castFen2YuanByBigDecimal(separateOrder.getTradeAmount()))
                    .setTradeTime(separateOrder.getCtime())
                    .setReconcileDate(new Date());
            reconcileTradeErrorDtoList.add(dto);
        }
    }

    private List<TradeErrorRecords> findFankSingleSideSeparateOrderRecords(LocalDateTime beginTime, LocalDateTime endTime) {
        // 查单边记录
        List<FbankPayRecords> unmatchedFbankPayRecords = fbankPayRecordsMapper.queryFbankSingleSideWithSeparateOrder(beginTime, endTime, ReconcileTradeConstants.reconcileSeparateOrderTradeCodes1);

        // 查询会员间交易退款单边记录 (特殊处理)  @see ReconcileTradeConstants.reconcileSeparateOrderTradeCodes2
        List<FbankPayRecords> unmatchedRefundFbankPayRecords = fbankPayRecordsMapper.queryFbankSingleSideWithRefund(beginTime, endTime);

        // 合并两类单边记录
        List<FbankPayRecords> unmatchedAllFbankPayRecords = ListUtils.mergeLists(unmatchedFbankPayRecords, unmatchedRefundFbankPayRecords);
        if (CollectionUtils.isEmpty(unmatchedAllFbankPayRecords)) {
            return Collections.emptyList();
        }

        // 填充 reconcileTradeErrorDtoList
        List<TradeErrorRecords> reconcileTradeErrorDtoList = new ArrayList<>();
        fillFankSingleSideReconcileTradeErrorDtoList(reconcileTradeErrorDtoList, unmatchedAllFbankPayRecords);
        return reconcileTradeErrorDtoList;
    }

    private void fillFankSingleSideReconcileTradeErrorDtoList(List<TradeErrorRecords> reconcileTradeErrorDtoList,
                                                              List<FbankPayRecords> unmatchedFbankPayRecords) {
        for (FbankPayRecords fbank : unmatchedFbankPayRecords) {
            TradeErrorRecords dto = new TradeErrorRecords()
                    .setBizNo(fbank.getBusinessNo())
                    //.setYbmOrderNo()
                    .setDataType(ReconcileTradeErrorDataTypeEnum.SETTLE_ERROR.getCode())
                    .setErrorType(ReconcileTradeErrorTypeEnum.SINGLE_SIDE.getCode())
                    .setErrorDescription(String.join("|",
                            ReconcileTradeErrorDataTypeEnum.SETTLE_ERROR.getType(),
                            FbankTradeCodeEnum.code2DescMap.get(fbank.getTradeCode()),
                            ReconcileTradeErrorTypeEnum.SINGLE_SIDE.getType(),
                            fbank.getBusinessNo()))
                    .setItemCode(ReconciliationType.TRADE_DETAIL.name())
                    .setTradeCode(fbank.getTradeCode())
                    //.setBizCode()
                    .setProcessStatus(ReconcileTradeErrorProcessStatus.PENDING.getCode())
                    .setExpectedAmount(BigDecimal.ZERO)  // 单边账 预期金额为0
                    .setActualAmount(new BigDecimal(fbank.getAmount()))
                    .setTradeTime(fbank.getPayTime())
                    .setReconcileDate(new Date());
            reconcileTradeErrorDtoList.add(dto);
        }
    }

}